import vendor from '../core/vendor';
import Base from '../core/Base';

var SHADER_STATE_TO_ENABLE = 1;
var SHADER_STATE_KEEP_ENABLE = 2;
var SHADER_STATE_PENDING = 3;

// Enable attribute operation is global to all programs
// Here saved the list of all enabled attribute index
// http://www.mjbshaw.com/2013/03/webgl-fixing-invalidoperation.html
var enabledAttributeList = {};

// some util functions
function addLineNumbers(string) {
    var chunks = string.split('\n');
    for (var i = 0, il = chunks.length; i < il; i ++) {
        // Chrome reports shader errors on lines
        // starting counting from 1
        chunks[i] = (i + 1) + ': ' + chunks[i];
    }
    return chunks.join('\n');
}

// Return true or error msg if error happened
function checkShaderErrorMsg(_gl, shader, shaderString) {
    if (!_gl.getShaderParameter(shader, _gl.COMPILE_STATUS)) {
        return [_gl.getShaderInfoLog(shader), addLineNumbers(shaderString)].join('\n');
    }
}

var tmpFloat32Array16 = new vendor.Float32Array(16);

var GLProgram = Base.extend({

    uniformSemantics: {},
    attributes: {}

}, function () {
    this._locations = {};

    this._textureSlot = 0;

    this._program = null;
}, {

    bind: function (renderer) {
        this._textureSlot = 0;
        renderer.gl.useProgram(this._program);
    },

    hasUniform: function (symbol) {
        var location = this._locations[symbol];
        return location !== null && location !== undefined;
    },

    useTextureSlot: function (renderer, texture, slot) {
        if (texture) {
            renderer.gl.activeTexture(renderer.gl.TEXTURE0 + slot);
            // Maybe texture is not loaded yet;
            if (texture.isRenderable()) {
                texture.bind(renderer);
            }
            else {
                // Bind texture to null
                texture.unbind(renderer);
            }
        }
    },

    currentTextureSlot: function () {
        return this._textureSlot;
    },

    resetTextureSlot: function (slot) {
        this._textureSlot = slot || 0;
    },

    takeCurrentTextureSlot: function (renderer, texture) {
        var textureSlot = this._textureSlot;

        this.useTextureSlot(renderer, texture, textureSlot);

        this._textureSlot++;

        return textureSlot;
    },

    setUniform: function (_gl, type, symbol, value) {
        var locationMap = this._locations;
        var location = locationMap[symbol];
        // Uniform is not existed in the shader
        if (location === null || location === undefined) {
            return false;
        }

        switch (type) {
            case 'm4':
                if (!(value instanceof Float32Array)) {
                    // Use Float32Array is much faster than array when uniformMatrix4fv.
                    for (var i = 0; i < value.length; i++) {
                        tmpFloat32Array16[i] = value[i];
                    }
                    value = tmpFloat32Array16;
                }
                _gl.uniformMatrix4fv(location, false, value);
                break;
            case '2i':
                _gl.uniform2i(location, value[0], value[1]);
                break;
            case '2f':
                _gl.uniform2f(location, value[0], value[1]);
                break;
            case '3i':
                _gl.uniform3i(location, value[0], value[1], value[2]);
                break;
            case '3f':
                _gl.uniform3f(location, value[0], value[1], value[2]);
                break;
            case '4i':
                _gl.uniform4i(location, value[0], value[1], value[2], value[3]);
                break;
            case '4f':
                _gl.uniform4f(location, value[0], value[1], value[2], value[3]);
                break;
            case '1i':
                _gl.uniform1i(location, value);
                break;
            case '1f':
                _gl.uniform1f(location, value);
                break;
            case '1fv':
                _gl.uniform1fv(location, value);
                break;
            case '1iv':
                _gl.uniform1iv(location, value);
                break;
            case '2iv':
                _gl.uniform2iv(location, value);
                break;
            case '2fv':
                _gl.uniform2fv(location, value);
                break;
            case '3iv':
                _gl.uniform3iv(location, value);
                break;
            case '3fv':
                _gl.uniform3fv(location, value);
                break;
            case '4iv':
                _gl.uniform4iv(location, value);
                break;
            case '4fv':
                _gl.uniform4fv(location, value);
                break;
            case 'm2':
            case 'm2v':
                _gl.uniformMatrix2fv(location, false, value);
                break;
            case 'm3':
            case 'm3v':
                _gl.uniformMatrix3fv(location, false, value);
                break;
            case 'm4v':
                // Raw value
                if (Array.isArray(value) && Array.isArray(value[0])) {
                    var array = new vendor.Float32Array(value.length * 16);
                    var cursor = 0;
                    for (var i = 0; i < value.length; i++) {
                        var item = value[i];
                        for (var j = 0; j < 16; j++) {
                            array[cursor++] = item[j];
                        }
                    }
                    _gl.uniformMatrix4fv(location, false, array);
                }
                else {   // ArrayBufferView
                    _gl.uniformMatrix4fv(location, false, value);
                }
                break;
        }
        return true;
    },

    setUniformOfSemantic: function (_gl, semantic, val) {
        var semanticInfo = this.uniformSemantics[semantic];
        if (semanticInfo) {
            return this.setUniform(_gl, semanticInfo.type, semanticInfo.symbol, val);
        }
        return false;
    },

    // Used for creating VAO
    // Enable the attributes passed in and disable the rest
    // Example Usage:
    // enableAttributes(renderer, ["position", "texcoords"])
    enableAttributes: function (renderer, attribList, vao) {
        var _gl = renderer.gl;
        var program = this._program;

        var locationMap = this._locations;

        var enabledAttributeListInContext;
        if (vao) {
            enabledAttributeListInContext = vao.__enabledAttributeList;
        }
        else {
            enabledAttributeListInContext = enabledAttributeList[renderer.__uid__];
        }
        if (!enabledAttributeListInContext) {
            // In vertex array object context
            // PENDING Each vao object needs to enable attributes again?
            if (vao) {
                enabledAttributeListInContext
                    = vao.__enabledAttributeList
                    = [];
            }
            else {
                enabledAttributeListInContext
                    = enabledAttributeList[renderer.__uid__]
                    = [];
            }
        }
        var locationList = [];
        for (var i = 0; i < attribList.length; i++) {
            var symbol = attribList[i];
            if (!this.attributes[symbol]) {
                locationList[i] = -1;
                continue;
            }
            var location = locationMap[symbol];
            if (location == null) {
                location = _gl.getAttribLocation(program, symbol);
                // Attrib location is a number from 0 to ...
                if (location === -1) {
                    locationList[i] = -1;
                    continue;
                }
                locationMap[symbol] = location;
            }
            locationList[i] = location;

            if (!enabledAttributeListInContext[location]) {
                enabledAttributeListInContext[location] = SHADER_STATE_TO_ENABLE;
            }
            else {
                enabledAttributeListInContext[location] = SHADER_STATE_KEEP_ENABLE;
            }
        }

        for (var i = 0; i < enabledAttributeListInContext.length; i++) {
            switch (enabledAttributeListInContext[i]){
                case SHADER_STATE_TO_ENABLE:
                    _gl.enableVertexAttribArray(i);
                    enabledAttributeListInContext[i] = SHADER_STATE_PENDING;
                    break;
                case SHADER_STATE_KEEP_ENABLE:
                    enabledAttributeListInContext[i] = SHADER_STATE_PENDING;
                    break;
                // Expired
                case SHADER_STATE_PENDING:
                    _gl.disableVertexAttribArray(i);
                    enabledAttributeListInContext[i] = 0;
                    break;
            }
        }

        return locationList;
    },

    getAttribLocation: function (_gl, symbol) {
        var locationMap = this._locations;

        var location = locationMap[symbol];
        if (location == null) {
            location = _gl.getAttribLocation(this._program, symbol);
            locationMap[symbol] = location;
        }

        return location;
    },

    isAttribEnabled: function (renderer, location) {
        var enabledAttributeListInContext
            = enabledAttributeList[renderer.__uid__]
            || [];

        return !!enabledAttributeListInContext[location];
    },

    buildProgram: function (_gl, shader, vertexShaderCode, fragmentShaderCode) {
        var vertexShader = _gl.createShader(_gl.VERTEX_SHADER);
        var program = _gl.createProgram();

        _gl.shaderSource(vertexShader, vertexShaderCode);
        _gl.compileShader(vertexShader);

        var fragmentShader = _gl.createShader(_gl.FRAGMENT_SHADER);
        _gl.shaderSource(fragmentShader, fragmentShaderCode);
        _gl.compileShader(fragmentShader);

        var msg = checkShaderErrorMsg(_gl, vertexShader, vertexShaderCode);
        if (msg) {
            return msg;
        }
        msg = checkShaderErrorMsg(_gl, fragmentShader, fragmentShaderCode);
        if (msg) {
            return msg;
        }

        _gl.attachShader(program, vertexShader);
        _gl.attachShader(program, fragmentShader);
        // Force the position bind to location 0;
        if (shader.attributeSemantics['POSITION']) {
            _gl.bindAttribLocation(program, 0, shader.attributeSemantics['POSITION'].symbol);
        }
        else {
            // Else choose an attribute and bind to location 0;
            var keys = Object.keys(this.attributes);
            _gl.bindAttribLocation(program, 0, keys[0]);
        }

        _gl.linkProgram(program);

        _gl.deleteShader(vertexShader);
        _gl.deleteShader(fragmentShader);

        this._program = program;

        // Save code.
        this.vertexCode = vertexShaderCode;
        this.fragmentCode = fragmentShaderCode;

        if (!_gl.getProgramParameter(program, _gl.LINK_STATUS)) {
            return 'Could not link program\n' + _gl.getProgramInfoLog(program);
        }

        // Cache uniform locations
        for (var i = 0; i < shader.uniforms.length; i++) {
            var uniformSymbol = shader.uniforms[i];
            this._locations[uniformSymbol] = _gl.getUniformLocation(program, uniformSymbol);
        }

    }
});

export default GLProgram;